﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.Entity;
using NetFrameworkExtensions.DataLayer.EF4.Properties;
using System.Data;
using System.Data.Entity.Infrastructure;
using System.Data.Common;
using System.Data.Objects;

namespace NetFrameworkExtensions.DataLayer.EF4
{
  public abstract class DbContextBase :
    DbContext
  {
    #region [ Constructors ]
    public DbContextBase()
      : base()
    {
    }

    public DbContextBase(DbCompiledModel model)
      : base(model)
    {
    }

    public DbContextBase(string nameOrConnectionString)
      : base(nameOrConnectionString)
    {

    }

    public DbContextBase(DbConnection existingConnection, bool contextOwnsConnection)
      : base(existingConnection, contextOwnsConnection)
    {

    }

    public DbContextBase(ObjectContext objectContext, bool dbContextOwnsObjectContext)
      : base(objectContext, dbContextOwnsObjectContext)
    {

    }

    public DbContextBase(string nameOrConnectionString, DbCompiledModel model)
      : base(nameOrConnectionString, model)
    {

    }

    public DbContextBase(DbConnection existingConnection, DbCompiledModel model, bool contextOwnsConnection)
      : base(existingConnection, model, contextOwnsConnection)
    {

    }
    #endregion

    public event EventHandler SaveChangesComplete;

    protected virtual void OnSaveChangesComplete()
    {
      var handler = SaveChangesComplete;
      if (handler != null)
        handler(this, EventArgs.Empty);
    }

    private List<Action> _OnSaveActions;

    public void AddSaveAction(Action action)
    {
      if (action == null) throw new ArgumentNullException("action");
      if (_OnSaveActions == null)
        _OnSaveActions = new List<Action>();
      _OnSaveActions.Add(action);
    }

    public override int SaveChanges()
    {
      DateTime now = DateTime.Now;

      var entries = ChangeTracker.Entries().ToArray();
      foreach (var entry in entries)
      {
        var entity = entry.Entity;
        var identified = entity as IIdentifiedData;
        var dateAuditable = entity as IDateTimeAuditableData;
        var concurrent = entity as IIdentifiedData;
        //var hierarchy = entity as IHierarchicalData;

        switch (entry.State)
        {
          case System.Data.EntityState.Added:
            {
              if (identified != null)
                AddedIdentified(identified, entry);

              if (dateAuditable != null)
                AddedDateTimeAuditable(dateAuditable, now, entry);

              AddedEntity(entry);
            }
            break;
          case System.Data.EntityState.Deleted:
            {
              if (dateAuditable != null)
                DeletedDateTimeAuditable(dateAuditable, now, entry);

              DeletedEntity(entry);
            }
            break;
          case System.Data.EntityState.Modified:
            {
              if (dateAuditable != null)
                UpdatedDateTimeAuditable(dateAuditable, now, entry);

              UpdatedEntity(entry);
            }
            break;
          default:
            break;
        }
      }

      var result = base.SaveChanges();

      if (_OnSaveActions != null)
      {
        var toIterate = _OnSaveActions.ToArray();
        _OnSaveActions = null;
        foreach (var action in toIterate)
          action();
      }

      OnSaveChangesComplete();

      return result;
    }

    #region [ Added Entities ]
    protected virtual void AddedEntity(DbEntityEntry entry)
    {
    }
    protected virtual void AddedIdentified(IIdentifiedData identified, DbEntityEntry entry)
    {
      if (identified is IIdentifierRequired)
      {
        if (identified.IdentifierType == typeof(Guid))
        {
          if (Object.Equals(identified.Identifier, Guid.Empty))
            identified.Identifier = Guid.NewGuid();
        }
        else
          throw new NotSupportedException(FormattedResources.EntityRequiresIdentifier(entry.Entity.GetType().Name));
      }
    }
    protected virtual void AddedDateTimeAuditable(IDateTimeAuditableData dateAuditable, DateTime now, DbEntityEntry entry)
    {
      dateAuditable.CreationDate = now;
      dateAuditable.ModificationDate = now;
    }
    #endregion

    #region [ Updated Entities ]
    protected virtual void UpdatedEntity(DbEntityEntry entry)
    {
    }
    protected virtual void UpdatedDateTimeAuditable(IDateTimeAuditableData dateAuditable, DateTime now, DbEntityEntry entry)
    {
      dateAuditable.ModificationDate = now;
    }
    #endregion

    #region [ Deleted Entities ]
    protected virtual void DeletedEntity(DbEntityEntry entry)
    {
    }
    protected virtual void DeletedDateTimeAuditable(IDateTimeAuditableData dateAuditable, DateTime now, DbEntityEntry entry)
    {
      if (dateAuditable.DeletionDate == null)
      {
        entry.State = EntityState.Unchanged;
        dateAuditable.DeletionDate = now;
        //if (userAuditable != null && userIdValid)
        //  userAuditable.DeletedById = userId;
      }
    }
    #endregion

  }

  public abstract class DbContextBase<TUserIdentifier, TProjectIdentifier> :
    DbContextBase
    where TUserIdentifier : IEquatable<TUserIdentifier>
    where TProjectIdentifier : IEquatable<TProjectIdentifier>
  {

    protected override void AddedEntity(DbEntityEntry entry)
    {
      base.AddedEntity(entry);

      var userAuditable = entry.Entity as IUserAuditableData;
      var projectFramed = entry.Entity as IProjectFramedData;

      if (userAuditable != null)
        AddedUserAuditable(userAuditable, entry);

      if (projectFramed != null)
        AddedProjectFramed(projectFramed, entry);
    }
    protected virtual void AddedProjectFramed(IProjectFramedData projectFramed, DbEntityEntry entry)
    {
      TProjectIdentifier projectId;
      var projectIdValid = TryGetProjectId(out projectId);

      if (projectFramed is IRequiredProjectData && !projectIdValid)
        throw new NotSupportedException(FormattedResources.EntityRequiresProject(entry.Entity.GetType().Name));
      if (projectIdValid)
        projectFramed.ProjectId = projectId;
    }
    protected virtual void AddedUserAuditable(IUserAuditableData userAuditable, DbEntityEntry entry)
    {
      TUserIdentifier userId;
      var userIdValid = TryGetUserId(out userId);

      if (userIdValid)
      {
        userAuditable.CreatedById = userId;
        userAuditable.ModifiedById = userId;
      }
    }

    protected override void UpdatedEntity(DbEntityEntry entry)
    {
      base.UpdatedEntity(entry);
      var userAuditable = entry.Entity as IUserAuditableData;
      if (userAuditable != null)
        UpdatedUserAuditable(userAuditable, entry);
    }
    protected virtual void UpdatedUserAuditable(IUserAuditableData userAuditable, DbEntityEntry entry)
    {
      TUserIdentifier userId;
      var userIdValid = TryGetUserId(out userId);
      if (userIdValid)
        userAuditable.ModifiedById = userId;
    }

    protected override void DeletedEntity(DbEntityEntry entry)
    {
      base.DeletedEntity(entry);

      var userAuditable = entry.Entity as IUserAuditableData;
      if (userAuditable != null)
        DeletedUserAuditable(userAuditable);
    }
    protected virtual void DeletedUserAuditable(IUserAuditableData userAuditable)
    {
      TUserIdentifier userId;
      var userIdValid = TryGetUserId(out userId);
      if (userIdValid)
        userAuditable.DeletedById = userId;
    }

    protected virtual bool TryGetUserId(out TUserIdentifier userId)
    {
      userId = default(TUserIdentifier);
      return false;
    }
    protected virtual bool TryGetProjectId(out TProjectIdentifier projectId)
    {
      projectId = default(TProjectIdentifier);
      return false;
    }
  }

  public abstract class GuidDbContextBase :
    DbContextBase<Guid, Guid>
  {
  }
}
